package vip.liux.backend.application.serviceImpl.point;

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import vip.liux.application.dots.ListResultDto;
import vip.liux.application.dots.PagedResultDto;
import vip.liux.backend.application.service.customer.CustomerDto;
import vip.liux.backend.application.service.customer.CustomerService;
import vip.liux.backend.application.service.point.*;
import vip.liux.backend.infrastructure.code.BizCodeGenerator;
import vip.liux.backend.infrastructure.utils.ListJoinKit;
import vip.liux.contracts.models.point.*;
import vip.liux.contracts.repositories.point.IPointsDetailRepository;
import vip.liux.contracts.repositories.point.IPointsTransactionRepository;
import vip.liux.contracts.repositories.point.IPointsUsageRepository;
import vip.liux.contracts.shared.consts.PointRuleConstants;
import vip.liux.contracts.shared.enums.point.PointRuleType;
import vip.liux.contracts.shared.enums.point.TransactionStatus;
import vip.liux.core.exception.BusinessException;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static vip.liux.application.PageKit.toPage;
import static vip.liux.application.PageKit.toPaged;
import static vip.liux.contracts.shared.consts.GeneralConstants.MAX_BIG_DECIMAL;

/**
 * 积分交易服务实现类
 * 提供积分交易相关的业务逻辑实现，包括分页查询交易记录、批量充值积分、消费积分和退款积分等功能。
 */
@RequiredArgsConstructor
@Service
public class PointsTransactionServiceImpl implements PointsTransactionService {

    // 交易流水号生成器
    private final BizCodeGenerator codeGenerator;

    // 积分交易记录仓储
    private final IPointsTransactionRepository transactionRepo;

    // 积分明细记录仓储
    private final IPointsDetailRepository pointsDetailRepo;

    // 积分使用记录仓储
    private final IPointsUsageRepository usageRepository;

    // 用户积分账户领域服务
    private final PointsAccountDomainService accountDomainService;

    // 积分交易领域服务
    private final PointsTransactionDomainService transactionDomainService;

    // 积分规则领域服务
    private final PointsRuleDomainService pointsRuleDomainService;

    private final TransactionTemplate transactionTemplate;

    private final CustomerService customerService;
    /**
     * 分页查询积分交易流水
     *
     * @param userId 用户ID
     * @param input  查询条件
     * @return 分页结果，包含积分交易记录
     */
    @Override
    public PagedResultDto<PointsTransactionDto> queryAccount(Long userId, PointsTransactionQueryDto input) {
        Page<PointsTransactionDto> page = transactionRepo
                .findByUserId(userId, input.getTransactionNo(), input.getRelatedNo(), input.getRuleCode(), input.getStart(), input.getEnd(), toPage(input))
                .map(AutoMapperProfile.INSTANCE::toDto);

        if (page.isEmpty()) {
            return new PagedResultDto<>();
        }

        List<PointsTransactionDto> list = page.getContent();
        ListResultDto<CustomerDto> customers = customerService.findByUserIds(list.stream().map(PointsTransactionDto::getUserId).toList());

        ListJoinKit.backFill(
                list,
                customers.getItems(),
                PointsTransactionDto::getUserId,
                CustomerDto::getId,
                PointsTransactionDto::setUser
        );


        return toPaged(page);
    }

    /**
     * 批量充值积分
     *
     * @param input 充值积分的请求列表
     * @return 充值结果，key为订单号，value为交易记录Dto
     */
    @Override
    public Map<String, PointsTransactionDto> batchRechargePoints(List<PointsRechargeDto> input) {
        Map<String, PointsTransactionDto> result = new HashMap<>();
        for (PointsRechargeDto earnDto : input) {
            // 调用单次充值方法
            PointsTransactionDto transactionDto = rechargePoints(earnDto);
            result.put(earnDto.getOrderNo(), transactionDto);
        }
        return result;
    }

    /**
     * 单次充值积分
     *
     * @param rechargeDto 充值积分的请求
     * @return 充值结果，交易记录Dto
     */
    @Override
    public PointsTransactionDto rechargePoints(PointsRechargeDto rechargeDto) {

        // 校验输入参数
        if (rechargeDto.getPoints().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("积分必须大于0");
        }
        if (rechargeDto.getPoints().compareTo(MAX_BIG_DECIMAL) > 0) {
            throw new IllegalArgumentException("积分不能超过最大值");
        }

        // 使用事务模板执行积分充值逻辑
        PointsTransaction transaction = transactionTemplate.execute(status -> {

            // 新增用户积分账户记录
            PointsAccount account = accountDomainService.addAvailablePoints(rechargeDto.getUserId(), rechargeDto.getPoints());

            // 校验积分规则
            PointsRule rule = pointsRuleDomainService.validateRule(PointRuleConstants.RECHARGE_ACQUIRE, rechargeDto.getUserId(), rechargeDto.getPoints());

            // 创建交易记录
            PointsTransaction savedTransaction = createTransaction(
                    rechargeDto.getUserId(),
                    rechargeDto.getPoints(),
                    rule,
                    rechargeDto.getOrderNo(),
                    rechargeDto.getDescription()
            );

            // 构建积分明细记录
            PointsDetail pointsDetail = buildPointDetail(savedTransaction, rule);

            return savedTransaction;
        });

        // 返回交易记录 Dto
        return AutoMapperProfile.INSTANCE.toDto(transaction);
    }

    /**
     * 批量消费积分
     *
     * @param input 扣除积分的请求列表
     * @return 扣除结果，key为订单号，value为交易记录Dto
     * @throws BusinessException 如果积分不足或规则校验失败
     */
    @Override
    public Map<String, PointsTransactionDto> batchConsumePoints(List<PointsConsumeDto> input) {
        Map<String, PointsTransactionDto> result = new HashMap<>();
        for (PointsConsumeDto consumeDto : input) {
            // 调用单次消费方法
            PointsTransactionDto transactionDto = consumePoints(consumeDto);
            result.put(consumeDto.getOrderNo(), transactionDto);
        }
        return result;
    }

    /**
     * 单次消费积分
     *
     * @param consumeDto 扣除积分的请求
     * @return 扣除结果，交易记录Dto
     * @throws BusinessException 如果积分不足或规则校验失败
     */
    @Override
    public PointsTransactionDto consumePoints(PointsConsumeDto consumeDto) {

        // 使用事务模板执行积分消费逻辑
        PointsTransaction transaction = transactionTemplate.execute(status -> {

            // 新增用户积分账户记录
            PointsAccount account = accountDomainService.addAvailablePoints(consumeDto.getUserId(), consumeDto.getPoints().negate());

            // 校验积分规则
            PointsRule rule = pointsRuleDomainService.validateRule(PointRuleConstants.CONSUME_RULE, consumeDto.getUserId(), consumeDto.getPoints());

            // 创建交易记录并设置初始状态为 PENDING
            PointsTransaction savedTransaction = createTransaction(
                    consumeDto.getUserId(),
                    consumeDto.getPoints(),
                    rule,
                    consumeDto.getOrderNo(),
                    consumeDto.getDescription()
            );

            // 抵扣积分
            deductPoints(consumeDto, savedTransaction);

            return savedTransaction;
        });

        // 返回交易记录 Dto
        return AutoMapperProfile.INSTANCE.toDto(transaction);
    }

    /**
     * 退款积分
     *
     * @param refundDto 退款积分的请求
     * @return 退款结果，交易记录Dto
     * @throws BusinessException 如果原交易不存在、未完成或退款积分超限
     */
    @Override
    public PointsTransactionDto refundPoints(PointsRefundDto refundDto) {
        PointsTransaction refundTransaction = transactionTemplate.execute(status -> {
            // 校验原交易记录
            PointsTransaction originalTransaction = validateOriginalTransaction(refundDto.getOriginalTransactionNo());

            // 校验退款积分
            validateRefundPoints(refundDto, originalTransaction);

            // 使用事务模板执行积分退款逻辑

            // 更新用户积分账户
            accountDomainService.addAvailablePoints(originalTransaction.getUserId(), refundDto.getPoints());

            // 校验积分规则
            PointsRule rule = pointsRuleDomainService.validateRule(PointRuleConstants.REFUND_ACQUIRE, originalTransaction.getUserId(), refundDto.getPoints());

            // 退款原交易记录
            transactionDomainService.refundTransaction(originalTransaction);

            // 创建退款交易记录并设置初始状态为 PENDING
            PointsTransaction saveTransaction = createTransaction(
                    originalTransaction.getUserId(),
                    refundDto.getPoints(),
                    rule, refundDto.getOrderNo(),
                    refundDto.getDescription()
            );

            // 构建积分明细记录
            PointsDetail pointsDetail = buildPointDetail(saveTransaction, rule);

            return saveTransaction;
        });

        // 返回退款交易记录 Dto
        return AutoMapperProfile.INSTANCE.toDto(refundTransaction);
    }

    /**
     * 校验原交易记录是否存在且已完成
     *
     * @param originalTransactionNo 原交易流水号
     * @return 原交易记录
     * @throws BusinessException 如果原交易不存在或未完成
     */
    private PointsTransaction validateOriginalTransaction(String originalTransactionNo) {
        PointsTransaction originalTransaction = transactionRepo.findByTransactionNo(originalTransactionNo)
                .orElseThrow(() -> new BusinessException("原交易记录不存在"));
        if (!TransactionStatus.COMPLETED.equals(originalTransaction.getStatus())) {
            throw new BusinessException("原交易未完成，无法退款");
        }
        return originalTransaction;
    }

    /**
     * 校验退款积分是否超限
     *
     * @param refundDto           退款请求数据
     * @param originalTransaction 原交易记录
     * @throws BusinessException 如果退款积分总和超过原交易积分
     */
    private void validateRefundPoints(PointsRefundDto refundDto, PointsTransaction originalTransaction) {
        BigDecimal totalRefundedPoints = transactionRepo
                .findByRelatedNo(refundDto.getOriginalTransactionNo())
                .stream()
                .filter(t -> TransactionStatus.COMPLETED.equals(t.getStatus()))
                .map(PointsTransaction::getPoints)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        if (refundDto.getPoints().add(totalRefundedPoints).compareTo(originalTransaction.getPoints()) > 0) {
            throw new BusinessException("退款积分总和不能大于原交易积分");
        }
    }

    /**
     * 创建(待交易)积分交易记录
     *
     * @param userId      用户ID
     * @param points      积分数量
     * @param rule        交易规则编码
     * @param orderNo     订单号
     * @param description 交易描述
     * @return 保存后的交易记录
     */
    private PointsTransaction createTransaction(Long userId, BigDecimal points, PointsRule rule, String orderNo, String description) {
        PointsTransaction transaction = new PointsTransaction();
        transaction.setUserId(userId);
        transaction.setPoints(rule.getRuleType() == PointRuleType.ACQUIRE ? points : points.negate());
        transaction.setRuleCode(rule.getRuleCode());
        transaction.setTransactionNo(codeGenerator.generateTransactionNo());
        transaction.setRelatedNo(orderNo);
        transaction.setDescription(description);
        transaction.setStatus(TransactionStatus.COMPLETED);
        return transactionDomainService.savePointsTransaction(transaction);
    }

    /**
     * 抵扣积分
     *
     * @param consumeDto  消费积分的请求数据
     * @param transaction 当前交易记录
     * @throws BusinessException 如果积分不足
     */
    private void deductPoints(PointsConsumeDto consumeDto, PointsTransaction transaction) {
        // 查询用户的积分明细并按到期时间排序
        List<PointsDetail> pointsDetails = pointsDetailRepo.findByUserId(consumeDto.getUserId())
                .stream()
                .filter(detail -> detail.getExpiryTime().isAfter(LocalDateTime.now()))
                .sorted(Comparator.comparing(PointsDetail::getExpiryTime))
                .toList();

        // 查询积分明细的使用记录
        List<PointsUsage> usages = usageRepository.findByDetailIdIn(
                pointsDetails.stream().map(PointsDetail::getId).collect(Collectors.toList())
        );

        BigDecimal remainingPoints = consumeDto.getPoints(); // 剩余需要抵扣的积分

        for (PointsDetail detail : pointsDetails) {
            // 计算该明细已使用的积分
            BigDecimal usedPoints = calculateUsedPoints(usages, detail.getId());
            BigDecimal availablePoints = detail.getPoints().subtract(usedPoints); // 计算剩余可用积分

            if (availablePoints.compareTo(BigDecimal.ZERO) > 0) {
                // 抵扣积分
                BigDecimal deduction = remainingPoints.min(availablePoints);
                remainingPoints = remainingPoints.subtract(deduction);

                // 保存使用记录
                saveUsageRecord(detail.getId(), consumeDto.getOrderNo(), deduction);

                if (remainingPoints.compareTo(BigDecimal.ZERO) <= 0) {
                    break; // 抵扣完成
                }
            }
        }

        if (remainingPoints.compareTo(BigDecimal.ZERO) > 0) {
            throw new BusinessException("积分不足，无法完成扣减");
        }
    }

    /**
     * 计算积分明细已使用的积分
     *
     * @param usages   使用记录列表
     * @param detailId 积分明细ID
     * @return 已使用的积分总数
     */
    private BigDecimal calculateUsedPoints(List<PointsUsage> usages, Long detailId) {
        return usages.stream()
                .filter(usage -> usage.getDetailId().equals(detailId))
                .map(PointsUsage::getUsedPoints)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    /**
     * 保存积分使用记录
     *
     * @param detailId  积分明细ID
     * @param orderNo   订单编号
     * @param deduction 抵扣的积分
     */
    private void saveUsageRecord(Long detailId, String orderNo, BigDecimal deduction) {
        PointsUsage usage = new PointsUsage();
        usage.setDetailId(detailId);
        usage.setUsedPoints(deduction);
        usage.setOrderNo(orderNo);
        usageRepository.save(usage);
    }

    /**
     * 构建积分明细记录
     *
     * @param transaction 积分交易记录
     * @param rule        积分规则
     * @return 保存后的积分明细记录
     */
    private PointsDetail buildPointDetail(PointsTransaction transaction, PointsRule rule) {
        PointsDetail pointsDetail = new PointsDetail();
        pointsDetail.setTransactionNo(transaction.getTransactionNo());
        pointsDetail.setUserId(transaction.getUserId());
        pointsDetail.setPoints(transaction.getPoints());
        pointsDetail.setRuleCode(rule.getRuleCode());
        pointsDetail.setExpiryTime(rule.calculateExpiryTime(LocalDateTime.now()));
        return pointsDetailRepo.save(pointsDetail);
    }
}